docs(design): forge state-transition write op record (RIG-3331) - #981
Merged
trunk-io[bot] merged 3 commits intoSep 7, 2026
Merged
Conversation
Design record for the forge state-transition write op: an agent sets an
issue/PR state on both GitHub and Linear through the same attribution
chokepoint every other forge write rides.
Portable {open, closed} core plus per-provider refinements (close_reason on
GitHub, workflow_state on Linear), fail-loud invalid_argument on a
refinement/provider mismatch, ErrUnsupported on the Linear PR half.
Transitions mutate an existing coordinate, so they are neither F3-deduped nor
DL-055-recorded, following the comment-arm precedent.
The load-bearing contract is actor attribution: the emitted STATE event must
carry the acting agent so RIG-3326's suppression arm can key on the real
transition actor rather than the DL-055 author-row proxy, which would wrongly
eat a human-close notification to the author.
Two load-bearing Open Questions are held for Matt: the actor-carrier mechanism
(OQ-1, a cross-record contract with RIG-3326) and the Linear default
workflow-state rule (OQ-2, a product-behavior call).
Refs RIG-3331
Co-authored-by: Matt Wilkinson <matt@rigel.build>
|
😎 Merged successfully - details. |
|
Compass engineering docs preview: https://compass-forge-rig-3331-state.compass-eng-docs.pages.dev Deployed from Changed pages: |
…d (RIG-3331) Adversarial review returned 4 high, 5 medium, 3 low. Fixes, each verified against the cited source: Two fabricated citations removed (rule://planning-evidence). The record named translateAttribution as an existing symbol - it does not exist anywhere in the tree; the real path is stripBodyToRef plus the detect-path attribution assignment. It also quoted 'deliberately never imports the store' as if from the ingest package; the actual term of art is the no-store rule. Both underlying claims were true, only the citations invented. The F3/DL-055 argument reached the right conclusion from the wrong premise. The comment arm is excluded because a comment is unrepresentable in the store index; a transition targets a coordinate that IS representable and often already occupied. The record inverted the risk as 'no row exists at the coordinate at all'. The real hazard is a row that DOES exist: RecordAuthoredArtifact's ON CONFLICT DO UPDATE overwrites client_request_id, which backs the F3 memo through a unique partial index, so routing a transition through record would destroy the original create's idempotency memo. Conclusion kept, reasoning replaced, hazard named. T1 was red by construction: it widened Provider while extending only the fake, but four compile-time satisfaction assertions live in that package, so the GitHub and Linear ones break immediately and T2/T3 (the repair) come after. T1 now lands the interface with all four implementors. The transition memo omitted tenant_id. Its sibling forge_authored_artifacts is tenant-keyed and defended by a two-tenants-same-coordinate test, so without it one tenant's memo could attribute another's STATE event. The RLS catalog test audits tables that carry the column, so an omission is invisible to it - the column now ships with three explicit obligations instead of a reliance on the guard. Three tasks silently depended on unruled Open Questions while the checklist presented nine unconditional slices. T3 is OQ-2-contingent, T4's memo half and all of T5 are OQ-1-contingent, each now tagged with what changes under the alternative ruling. Also: ruled the ambiguous-workflow-state-name case (Linear does not enforce name uniqueness within a team) rather than leaving an implementer to invent a rule on the write path; specified the workflow-state cache separately from the invalidation-free teamIDs cache, since humans rename and reorder states; named the webhook/sweep double-emission and cursor-advance races; stated the memo read's tenant context; pinned the table to 0001_init.sql per the collapsed-migration convention; corrected the DL enumeration to include PR #932's DL-341; resolved the T8 contradiction (the rows ship in the freeze push, because DL-343's substance is what OQ-1 asks Matt to rule, and stamping 'Active (Matt)' now would attribute a decision he has not made); and flipped Status to Draft. Refs RIG-3331 Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka
marked this pull request as ready for review
September 7, 2026 18:50
Both load-bearing Open Questions are ruled, so the record flips Draft -> Active and its ledger rows land in this push. - OQ-1: the actor carrier is the consumable `forge_state_transitions` memo, not a synthetic STATE event. RIG-3326 is the side whose text bends -- its STATE arm resolves the actor through a memo lookup rather than off the event body. T5 keeps its subject and T4's memo half is in scope. - OQ-2: Linear default-state resolution defaults only where the target is unambiguous and fails `invalid_argument` naming the candidates otherwise, replacing the positional tie-break. A mandatory `workflow_state` was also rejected: it forces a provider-aware state fetch into every callsite and splits GitHub/Linear ergonomics. Measured at freeze, the Rigel team has exactly one `completed`, one `unstarted` and one `backlog` state, so the original "silently picks a board column" premise had no choice to make -- the rejection arm is a guard against future drift, covered by fixture because no current board reproduces it. All nine task slices are now unconditional; T3 gains the multi-candidate rejection fixture. DL-342/DL-343 appended with the real ruling date, ids re-verified next-free against main (highest DL-337) and every open design PR (#913 338/339, #900 340, #932 341). Refs RIG-3331 Co-authored-by: Matt Wilkinson <matt@rigel.build>
mattwilkinsonn
approved these changes
Sep 7, 2026
trunk-io
Bot
deleted the
compass-forge/rig-3331-state-transition-design
branch
September 7, 2026 21:00
This was referenced Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Design record for the forge state-transition write op — an agent setting
an issue/PR's state on both GitHub and Linear, through the same attribution
chokepoint every other forge write rides. Today
forgeServicehas no sucharm and
forge.Providerhas no such method, so every STATE event reachingthe notify router is human- or externally-actored.
This push freezes the record (
Status: Active) and lands its ledger rows.Both Open Questions ruled (Matt, 2026-09-07)
OQ-1 — actor carrier: the consumable memo. The acting agent's identity
travels in a durable, tenant-scoped
forge_state_transitionsmemo, writtenafter a successful transition at the chokepoint and consumed on match at the
notify lane; the synthetic-event alternative is rejected as inventing an
event the forge never sent. Because RIG-3326's frozen text describes the
actor as stamped onto the emitted event, RIG-3326 is the side whose text
bends: its STATE arm resolves the actor through a memo lookup at the
actor-resolution seam. Identical suppression outcome, different mechanism.
Consequence: T5 has a subject, T4's memo half is in scope, DL-343 carries it.
OQ-2 — Linear default state: default when unambiguous, reject when not.
With
workflow_stateempty, resolve the sole candidate of the target typeand use it; on two or more candidates fail
invalid_argumentnaming them andrequiring an explicit
workflow_state. Both originally-offered options wererejected — a positional tie-break because it guesses silently and would start
guessing years later with no test watching, a mandatory
workflow_statebecause it forces a provider-aware state fetch into every callsite and splits
GitHub/Linear ergonomics.
OQ-2 was also ill-posed as filed, and saying so is part of the freeze. It
was surfaced as a product-behavior call about "silently choosing a
human-visible board column" — a premise never measured against the target
board. Measured at freeze, the Rigel team has eight workflow states of which
exactly one is
completed("Done"), oneunstarted("Todo") and onebacklog("Backlog"), so there was no column choice to make. The rulingconverts OQ-2 from a product call into a fail-loud guard against future
drift; because no current board reproduces the ambiguous case, the rejection
arm's only coverage is a fixture (added to T3).
Dependency direction
RIG-3331 is the prerequisite; RIG-3326 (#913) is the consumer. #913's own
body says its STATE arm "stays interim-open until RIG-3331 lands" — a
dispatch-ordering dependency. An earlier sequencing of mine had this arrow
backwards and gated the prerequisite behind its own consumer; Matt corrected
it. Landing T5 is what closes #913's interim-open arm.
Plan
Nine unconditional slices (all contingency tags cleared by the rulings):
proto arms →
Providerwidening with all four implementors together (or thepackage is red) → GitHub PATCH → Linear
issueUpdate+ workflow-stateresolution → server arms + memo → actor-resolution seam → tools → live-oracle
sweep → ledger.
Ledger impact
Ledger-impact: APPENDS DL-342 (the op: portable
{open, closed}core +per-provider refinements, fail-loud in-band on every mismatch including the
multi-candidate default,
ErrUnsupportedon the Linear PR half, and notF3-deduped/DL-055-recorded — because a transition's coordinate row is a
write-once authorship fact whose
client_request_idbacks the create's F3memo, so recording through that path would overwrite it) and DL-343 (the
memo mechanism and the RIG-3326 contract).
Ids re-verified next-free at freeze, against main and every open design
PR: main's highest is DL-337 (304 rows), #913 holds DL-338/339, #900 holds
DL-340, #932 holds DL-341. This check decays as main advances — it was re-run
here because main moved between the two pushes, and an earlier draft of the
numbering paragraph did collide (proposing DL-340, already claimed by #900).
Verification
moon cigreen — 18 actions, 0 failed.design-ledger-gateOK (306 rows,128 record headers valid);
root:markdownlint0 errors across 203 files.Design-record-only change: no Go, proto or SQL touched.
Spec-impact: none
Refs RIG-3331